package org.boticelli.plugin.twitter;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import oauth.signpost.OAuth;
import oauth.signpost.OAuthConsumer;
import oauth.signpost.OAuthProvider;
import oauth.signpost.exception.OAuthCommunicationException;
import oauth.signpost.exception.OAuthExpectationFailedException;
import oauth.signpost.exception.OAuthMessageSignerException;
import oauth.signpost.exception.OAuthNotAuthorizedException;

import org.boticelli.Bot;
import org.boticelli.auth.UserAction;
import org.boticelli.auth.UserActionExecutor;
import org.boticelli.dao.TwitterDAO;
import org.boticelli.model.TwitterAuth;
import org.boticelli.model.TwitterEntry;
import org.boticelli.plugin.HelpfulBoticelliPlugin;
import org.boticelli.plugin.PluginResult;
import org.boticelli.plugin.dist.AbstractBotAwarePlugin;
import org.boticelli.plugin.dist.ResponseMode;
import org.boticelli.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Required;

import f00f.net.irc.martyr.InCommand;
import f00f.net.irc.martyr.commands.MessageCommand;

public class TwitterOAuthPlugin
    extends AbstractBotAwarePlugin
    implements HelpfulBoticelliPlugin
{
    private static Logger log = LoggerFactory.getLogger(TwitterOAuthPlugin.class);
    
    
    private UserActionExecutor userActionExecutor;
    
    private TwitterDAO twitterDAO;
    
    private TwitterAPI twitterAPI;

    private String mainAccountSecret;
    
    private static ConcurrentMap<String, UserAuthState> waitingConsumers = new ConcurrentHashMap<String, UserAuthState>();
    
    @Required
    /**
     * Secret that needs to be given as second pin param to signal authentication of the bot main 
     * account and not the current user account.
     * 
     */
    public void setMainAccountSecret(String mainAccountSecret)
    {
        this.mainAccountSecret = mainAccountSecret;
    }
    
    @Required
    public void setTwitterAPI(TwitterAPI twitterAPI)
    {
        this.twitterAPI = twitterAPI;
    }

    public void setTwitterDAO(TwitterDAO twitterDAO)
    {
        this.twitterDAO = twitterDAO;
    }
    
    @Required
    public void setUserActionExecutor(UserActionExecutor userActionExecutor)
    {
        this.userActionExecutor = userActionExecutor;
    }

    @Override
    public String getHelpName()
    {
        return "oauth";
    }


    @Override
    public String helpText(Bot bot, List<String> args)
    {
        return "lets you (or the bot itself) be authenticated on teh twitter over OAuth";
    }


    @Override
    public PluginResult handle(Bot bot, InCommand command) throws Exception
    {
        MessageCommand messageCommand = (MessageCommand) command;
        
        String message = messageCommand.getMessage();

        List<String> words = Util.split(message, " ");
        
        int wordCount = words.size();
        if (wordCount > 0)
        {
            String verb = words.get(0);
            String nick = messageCommand.getSource().getNick();
            if (verb.equals("oauth"))
            {
                OAuthConsumer consumer = twitterAPI.createOAuthConsumer();
                
                OAuthProvider provider = twitterAPI.createOAuthProvider();
    
                log.debug("Fetching request token from Twitter...");
                String authUrl = provider.retrieveRequestToken(consumer, OAuth.OUT_OF_BAND);

                boolean authMain = (wordCount == 2 && words.get(1).equals(mainAccountSecret));
    
                StoreTokenAndWaitForPinAction action = new StoreTokenAndWaitForPinAction(consumer
                    .getToken(), consumer.getTokenSecret(), authUrl, authMain);
                userActionExecutor.execute(nick, action);
                
                
                waitingConsumers.put(nick, new UserAuthState(provider,consumer, authMain));
            }        
            else if (verb.equals("pin") && wordCount == 2)
            {
                
                String pin = words.get(1);
                AuthenticateWithPIN action = new AuthenticateWithPIN( pin);
                userActionExecutor.execute(nick, action);
            }
        }

        return PluginResult.NEXT;
    }



    @Override
    public boolean supports(Class<? extends InCommand> inCommandType)
    {
        return MessageCommand.class.isAssignableFrom(inCommandType);
    }

    
    public class StoreTokenAndWaitForPinAction implements UserAction
    {

        private String tokenSecret;
        private String token;
        private String authUrl;
        private boolean authMain;

        public StoreTokenAndWaitForPinAction(String token, String tokenSecret, String authUrl, boolean authMain)
        {
            this.token = token;
            this.tokenSecret = tokenSecret;
            this.authUrl = authUrl;
            this.authMain = authMain;
        }

        @Override
        public void execute(Bot bot, String userNick)
        {
            String twitterNick = authMain ? bot.getNick() : userNick;
            List<TwitterEntry> entries = twitterDAO.findByNick(twitterNick);
            boolean noEntry = entries.size() == 0;
            TwitterEntry entry;
            if (noEntry)
            {
                entry = new TwitterEntry();
            }
            else
            {
                entry = entries.get(0);
            }
            
            log.debug("StoreTokenAndWaitForPinAction entry = {}", entry);
            
            entry.setNick(twitterNick);
            entry.setToken(token);
            entry.setSecret(tokenSecret);
            entry.setTwitterAuth(TwitterAuth.WAIT_FOR_PIN);
            if (noEntry)
            {
                twitterDAO.create(entry);
            }
            else
            {
                twitterDAO.update(entry);
            }
            
            log.debug(" entry now = {}", entry);
            bot.sendMessage(userNick, "Visit " + authUrl + " to authorize me as twitter app. " +
            		"Then private message me in the form \"pin <PIN-code>\"");
        }
            

        @Override
        public String getDescription()
        {
            return "authorize for twitter";
        }

        @Override
        public ResponseMode getResponseMode()
        {
            return ResponseMode.PRIVATE;
        }
    }

    public class AuthenticateWithPIN implements UserAction
    {

        private String pin;

        public AuthenticateWithPIN(String pin)
        {
            this.pin = pin;
        }

        @Override
        public void execute(Bot bot, String userNick)
        {
            
            try
            {
                UserAuthState state = waitingConsumers.get(userNick);
                String twitterNick = state.isAuthMain() ? bot.getNick() : userNick;
                
                List<TwitterEntry> entries = twitterDAO.findByNick(twitterNick);
                if (entries.size() > 0)
                {
                    TwitterEntry entry = entries.get(0);
                    
                    if (entry.getTwitterAuth() != TwitterAuth.WAIT_FOR_PIN)
                    {
                        notWaitingError(bot, userNick);
                        return;
                    }
                    log.debug("Retrieve access token: entry = {}", entry);
                    
                    OAuthConsumer consumer = state.getConsumer();
                    if (consumer == null)
                    {
                        notWaitingError(bot, userNick);
                        return;
                    }
                    state.getProvider().retrieveAccessToken(consumer, pin );
                    
                    entry.setToken(consumer.getToken());
                    entry.setSecret(consumer.getTokenSecret());
                    entry.setTwitterAuth(TwitterAuth.OK);
                    
                    twitterDAO.update(entry);
                    bot.sendMessage(userNick, (state.isAuthMain() ? "You are" : "The bot is") + " now authenticated with twitter.");
                    waitingConsumers.remove(userNick);
                }
            }
            catch (OAuthMessageSignerException e)
            {
                throw new RuntimeException(e);
            }
            catch (OAuthNotAuthorizedException e)
            {
                throw new RuntimeException(e);
            }
            catch (OAuthExpectationFailedException e)
            {
                throw new RuntimeException(e);
            }
            catch (OAuthCommunicationException e)
            {
                throw new RuntimeException(e);
            }
        }

        private void notWaitingError(Bot bot, String nick)
        {
            bot.sendMessage(nick, "Not waiting for PIN. " +
                    "Have you used the oauth command to start the authentication process?");
        }

        @Override
        public String getDescription()
        {
            return "validate with the pin you gave me.";
        }

        @Override
        public ResponseMode getResponseMode()
        {
            return ResponseMode.PRIVATE;
        }
        
    }
}
